<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Wind Field Simulation r179.1</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            overflow: hidden;
        }
    </style>
    <script id="drawVert" type="x-shader/x-fragment">
        precision mediump float;

        attribute float a_index;

        uniform sampler2D u_particles;
        uniform float u_particles_res;

        varying vec2 v_particle_pos;

        void main() {
            vec4 color = texture2D(u_particles, vec2(
            fract(a_index / u_particles_res),
            floor(a_index / u_particles_res) / u_particles_res));

            // decode current particle position from the pixel's RGBA value
            v_particle_pos = vec2(
            color.r / 255.0 + color.b,
            color.g / 255.0 + color.a);

            gl_PointSize = 1.0;
            vec2 pos = vec2(2.0 * v_particle_pos.x - 1.0, 1.0 - 2.0 * v_particle_pos.y);
            gl_Position = vec4(pos, 0, 1);
        }
    </script>
    <script id="drawFrag" type="x-shader/x-fragment">
        precision mediump float;

        uniform sampler2D u_wind;
        uniform vec2 u_wind_min;
        uniform vec2 u_wind_max;
        uniform sampler2D u_color_ramp;

        varying vec2 v_particle_pos;

        void main() {
            vec2 vPos = vec2(v_particle_pos.x, 1.0 - v_particle_pos.y);
            vec2 velocity = mix(u_wind_min, u_wind_max, texture2D(u_wind, vPos).rg);
            float speed_t = length(velocity) / length(u_wind_max);

            // color ramp is encoded in a 16x16 texture
            vec2 ramp_pos = vec2(
            fract(16.0 * speed_t),
            floor(16.0 * speed_t) / 16.0);

            gl_FragColor = texture2D(u_color_ramp, ramp_pos);
            // gl_FragColor = texture2D(u_wind, v_particle_pos);
        }
    </script>
    <script id="quadVert" type="x-shader/x-fragment">
        precision mediump float;

        varying vec2 v_tex_pos;

        void main() {
            v_tex_pos = uv;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
    </script>
    <script id="screenFrag" type="x-shader/x-fragment">
        precision mediump float;

        uniform sampler2D u_screen;
        uniform float u_opacity;

        varying vec2 v_tex_pos;

        void main() {
//            vec4 color = texture2D(u_screen, 1.0 - v_tex_pos);
            vec4 color = texture2D(u_screen, v_tex_pos);
            // a hack to guarantee opacity fade out even with a value close to 1.0
            gl_FragColor = vec4(floor(255.0 * color * u_opacity) / 255.0);
        }
    </script>
    <script id="updateFrag" type="x-shader/x-fragment">
        precision highp float;

        uniform sampler2D u_particles;
        uniform sampler2D u_wind;
        uniform vec2 u_wind_res;
        uniform vec2 u_wind_min;
        uniform vec2 u_wind_max;
        uniform float u_rand_seed;
        uniform float u_speed_factor;
        uniform float u_drop_rate;
        uniform float u_drop_rate_bump;

        varying vec2 v_tex_pos;

        // pseudo-random generator
        const vec3 rand_constants = vec3(12.9898, 78.233, 4375.85453);
        float rand(const vec2 co) {
            float t = dot(rand_constants.xy, co);
            return fract(sin(t) * (rand_constants.z + t));
        }

        // wind speed lookup; use manual bilinear filtering based on 4 adjacent pixels for smooth interpolation
        vec2 lookup_wind(vec2 uv) {
            // return texture2D(u_wind, uv).rg; // lower-res hardware filtering
            uv.y = 1.0 - uv.y;
            vec2 px = 1.0 / u_wind_res;
            vec2 vc = (floor(uv * u_wind_res)) * px;
            vec2 f = fract(uv * u_wind_res);
            vec2 tl = texture2D(u_wind, vc).rg;
            vec2 tr = texture2D(u_wind, vc + vec2(px.x, 0)).rg;
            vec2 bl = texture2D(u_wind, vc + vec2(0, px.y)).rg;
            vec2 br = texture2D(u_wind, vc + px).rg;
            return mix(mix(tl, tr, f.x), mix(bl, br, f.x), f.y);
        }

        void main() {
            vec4 color = texture2D(u_particles, v_tex_pos);
            vec2 pos = vec2(
            color.r / 255.0 + color.b,
            color.g / 255.0 + color.a); // decode particle position from pixel RGBA

            vec2 velocity = mix(u_wind_min, u_wind_max, lookup_wind(pos));
            float speed_t = length(velocity) / length(u_wind_max);

            // take EPSG:4236 distortion into account for calculating where the particle moved
            float distortion = cos(radians(pos.y * 180.0 - 90.0));
            if (distortion == 0.0) {
                distortion = 0.001;
            }
            vec2 offset = vec2(velocity.x / distortion, -velocity.y) * 0.0001 * u_speed_factor;

            // update particle position, wrapping around the date line
            pos = fract(1.0 + pos + offset);

            // a random seed to use for the particle drop
            vec2 seed = (pos + v_tex_pos) * u_rand_seed;

            // drop rate is a chance a particle will restart at random position, to avoid degeneration
            float drop_rate = u_drop_rate + speed_t * u_drop_rate_bump;
            float drop = step(1.0 - drop_rate, rand(seed));

            vec2 random_pos = vec2(
            rand(seed + 1.3),
            rand(seed + 2.1));
            pos = mix(pos, random_pos, drop);

            // encode the new particle position back into RGBA
            gl_FragColor = vec4(
            fract(pos * 255.0),
            floor(pos * 255.0) / 255.0);

        }
    </script>

</head>
<body>
<script type="importmap">
    {
      "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.179.1/build/three.module.js",
        "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.179.1/examples/jsm/"
      }
    }
</script>
<script type="module">
    //= wind field simulation demo ================================================================

    // (8 August 2025)
    //
    // History: This is an update of a three.js wind field simulation created in 2025 by jeromehuang1984 (https://github.com/jeromehuang1984/challenge_wind_feild),
    // based on a 2016 js version created by Volodymyr Agafonkin (https://github.com/mapbox/webgl-wind) and adapted to three.js by jeromehuang1984
    //
    // Pavel Boytchev help a lot in adapt wind field simulation demo to three.js in August 2025
    //

    import * as THREE from 'three'
    import {OrbitControls} from "three/addons/controls/OrbitControls.js"

    const drawVert = document.getElementById("drawVert").textContent;
    const drawFrag = document.getElementById("drawFrag").textContent;
    const quadVert = document.getElementById("quadVert").textContent;
    const screenFrag = document.getElementById("screenFrag").textContent;
    const updateFrag = document.getElementById("updateFrag").textContent;

    function getJSON(url, callback) {
        const xhr = new XMLHttpRequest();
        xhr.responseType = 'json';
        xhr.open('get', url, true);
        xhr.onload = function () {
            if (xhr.status >= 200 && xhr.status < 300) {
                callback(xhr.response);
            } else {
                throw new Error(xhr.statusText);
            }
        };
        xhr.send();
    }

    const defaultRampColors = {
        0.0: '#3288bd',
        0.1: '#66c2a5',
        0.2: '#abdda4',
        0.3: '#e6f598',
        0.4: '#fee08b',
        0.5: '#fdae61',
        0.6: '#f46d43',
        1.0: '#d53e4f'
    };

    function getColorRamp(colors) {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');

        canvas.width = 256;
        canvas.height = 1;

        const gradient = ctx.createLinearGradient(0, 0, 256, 0);
        for (const stop in colors) {
            gradient.addColorStop(+stop, colors[stop]);
        }

        ctx.fillStyle = gradient;
        ctx.fillRect(0, 0, 256, 1);

        return new Uint8Array(ctx.getImageData(0, 0, 256, 1).data);
    }

    const renderer = new THREE.WebGLRenderer( { antialias: true } );
    renderer.setPixelRatio( window.devicePixelRatio );
    renderer.setSize( window.innerWidth, window.innerHeight );
    renderer.outputColorSpace = THREE.LinearSRGBColorSpace
    // renderer.autoClear = false;
    document.body.appendChild( renderer.domElement );

    const scene = new THREE.Scene();

    const width = window.innerWidth;
    const height = window.innerHeight;
    const camera = new THREE.PerspectiveCamera( 45, width / height, 0.01, 200 );
    camera.position.set( 0, 0, 1.42 );
    camera.up.set(0, 0, 1);
    camera.lookAt( 0, 0, 0 );

    const sceneRTT = new THREE.Scene();
    const sceneBack = new THREE.Scene();
    const sceneFront = new THREE.Scene();

    const opt = {
        magFilter: THREE.NearestFilter,
        minFilter: THREE.NearestFilter,
        format: THREE.RGBAFormat,
        type: THREE.UnsignedByteType,
        depthBuffer: false
    }
    const screenTarget = new THREE.WebGLRenderTarget( width, height, opt );
    const backgroundTarget = new THREE.WebGLRenderTarget( width, height, opt );

    const controls = new OrbitControls( camera, renderer.domElement );
    // controls.maxPolarAngle = Math.PI * 0.5;
    controls.minDistance = 0.01;
    controls.maxDistance = 100;

    scene.add( new THREE.AxesHelper(2000) )

    getJSON('./assets/ne_110m_coastline.geojson', function (data) {
        const positions = [];
        const geometry = new THREE.BufferGeometry();
        const material = new THREE.LineBasicMaterial( { color: '#fff' } );
        for (let i = 0; i < data.features.length; i++) {
            const line = data.features[i].geometry.coordinates;
            for (let j = 0; j < line.length; j++) {
                const x = (line[j][0] + 180) * 2 / 360;
                const y = (line[j][1] + 90) * 1 / 180;
                positions.push(x, y, 0)
                if (j && j < line.length - 1) {
                    positions.push(x, y, 0)
                }
            }
        }
        geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
        const contours = new THREE.LineSegments(geometry, material );
        contours.position.set( -1, -0.5, 0 );
        scene.add( contours );
    });

    const windFiles = {
        0: '2016112000',
        6: '2016112006',
        12: '2016112012',
        18: '2016112018',
        24: '2016112100',
        30: '2016112106',
        36: '2016112112',
        42: '2016112118',
        48: '2016112200'
    };

    async function updateWind(name) {
        return new Promise( ( resolve, reject ) => {
            getJSON('./assets/wind/' + windFiles[name] + '.json', function (windData) {
                const windImage = new Image();
                windData.image = windImage;
                windImage.src = './assets/wind/' + windFiles[name] + '.png';
                windImage.onload = function () {

                    resolve(windData);
                };
            });
        })
    }
    const windFileKey = 0
    const windData = await updateWind(windFileKey)
    console.log(windData)

    const particleRes = 256.0
    const _numParticles = particleRes * particleRes;
    const textureColors = new THREE.DataTexture(getColorRamp(defaultRampColors), 16, 16, THREE.RGBAFormat);
    textureColors.magFilter = THREE.LinearFilter;
    textureColors.minFilter = THREE.LinearFilter;
    textureColors.generateMipmaps = false;
    textureColors.needsUpdate = true;

    const pointsArr = new Float32Array(_numParticles * 3);
    for (let i = 0; i < pointsArr.length; i++) {
        pointsArr[i] = 0;
    }
    const pointsBuffer = new THREE.Float32BufferAttribute(pointsArr, 3)
    const particleIndices = new Float32Array(_numParticles);
    for (let i = 0; i < _numParticles; i++) {
        particleIndices[i] = i;
    }
    const particleIndexBuffer = new THREE.Float32BufferAttribute(particleIndices, 1)

    const particleState = new Uint8Array(_numParticles * 4);
    for (let i = 0; i < particleState.length; i++) {
        particleState[i] = Math.floor(Math.random() * 256.0); // randomize the initial particle positions
    }
    const initialPositionTexture = new THREE.DataTexture(particleState, particleRes, particleRes, THREE.RGBAFormat, THREE.UnsignedByteType);
    initialPositionTexture.needsUpdate = true;

    const WindParam = {
        fadeOpacity: 0.996, // how fast the particle trails fade on each frame
        speedFactor: 0.25, // how fast the particles move
        dropRate: 0.003, // how often the particles move to a random place
        dropRateBump: 0.01, // drop rate increase relative to individual particle speed
    }
    const windTexture = new THREE.Texture(windData.image);
    windTexture.magFilter = THREE.LinearFilter;
    windTexture.minFilter = THREE.LinearFilter;
    windTexture.format = THREE.RGBAFormat;
    windTexture.needsUpdate = true;

    const materialBg = new THREE.ShaderMaterial( {
        uniforms: {
            u_screen: { value: backgroundTarget.texture },
            u_opacity: { value: WindParam.fadeOpacity },
        },
        vertexShader: quadVert,
        fragmentShader: screenFrag,
        depthTest: false,
        transparent: true,
    } );
    const materialScreen = new THREE.ShaderMaterial( {
        uniforms: {
            u_screen: { value: screenTarget.texture },
            u_opacity: { value: 1.0 },
        },
        vertexShader: quadVert,
        fragmentShader: screenFrag,
        blending: THREE.CustomBlending,
        blendSrc: THREE.OneMinusSrcAlphaFactor, // SrcAlphaFactor OneMinusSrcAlphaFactor
        blendDst: THREE.SrcAlphaFactor,
        blendEquation: THREE.AddEquation,
        depthTest: false,
        transparent: true,
    } );

    const planeGeometry = new THREE.PlaneGeometry( 2, 1, 1 );
    const planeMaterial = new THREE.MeshBasicMaterial( {wireframe: false, color: 0xffffff, side: THREE.DoubleSide } );
    // planeMaterial.map = textureColors;
    // planeMaterial.map = windTexture;
    const planeMesh = new THREE.Mesh( planeGeometry, planeMaterial );
    scene.add(planeMesh);
    planeMesh.position.set( 0, 0, -0.001 );

    // copy from three.js\examples\jsm\postprocessing\Pass.js
    const _camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
    class FullscreenTriangleGeometry extends THREE.BufferGeometry {
        constructor() {
            super();
            this.setAttribute( 'position', new THREE.Float32BufferAttribute( [ - 1, 3, 0, - 1, - 1, 0, 3, - 1, 0 ], 3 ) );
            this.setAttribute( 'uv', new THREE.Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) );
        }
    }
    const _geometry = new FullscreenTriangleGeometry()
    // end copy from

    const meshBg = new THREE.Mesh(_geometry, materialBg)
    sceneBack.add( meshBg )
    const meshFront = new THREE.Mesh(_geometry, materialScreen)
    sceneFront.add(meshFront)

    const pointsWindGeo = new THREE.BufferGeometry();
    pointsWindGeo.setAttribute('a_index', particleIndexBuffer)
    pointsWindGeo.setAttribute('position', pointsBuffer)

    const particleStateTarget0 = new THREE.WebGLRenderTarget( particleRes, particleRes, opt );
    const particleStateTarget1 = new THREE.WebGLRenderTarget( particleRes, particleRes, opt );

    const materialUpdate = new THREE.ShaderMaterial( {
        uniforms: {
            u_particles: { value: initialPositionTexture },
            u_particles_res: { value: particleRes },
            u_wind_res: { value: {x: windData.width, y: windData.height} },
            u_wind: { value: windTexture },
            u_wind_min: { value: {x: windData.uMin, y: windData.vMin} },
            u_wind_max: { value: {x: windData.uMax, y: windData.vMax} },
            u_speed_factor: { value: WindParam.speedFactor },
            u_drop_rate: { value: WindParam.dropRate },
            u_drop_rate_bump: { value: WindParam.dropRateBump },
            u_rand_seed: { value: Math.random() },
        },
        depthTest: false,
        // transparent: true,
        vertexShader: quadVert,
        fragmentShader: updateFrag
    })
    const meshUpdate = new THREE.Mesh( _geometry, materialUpdate );
    sceneRTT.add( meshUpdate );
    function findRenderTargetNot(texture) {
        const targets = [particleStateTarget0, particleStateTarget1]
        return targets.find(target => target.texture !== texture);
    }
    function drawUpdateParticles() {
        const target = findRenderTargetNot(materialUpdate.uniforms.u_particles.value)
        renderer.setRenderTarget( target );
        // renderer.clear();
        materialUpdate.uniforms.u_rand_seed.value = Math.random();
        renderer.render( sceneRTT, _camera );
        materialUpdate.uniforms.u_particles.value = findRenderTargetNot(materialUpdate.uniforms.u_particles.value).texture;
    }
    const materialDraw = new THREE.ShaderMaterial( {
        uniforms: {
            u_particles: { value: particleStateTarget0.texture },
            u_particles_res: { value: particleRes },
            u_wind: { value: windTexture },
            u_color_ramp: { value: textureColors },
            u_wind_min: { value: {x: windData.uMin, y: windData.vMin} },
            u_wind_max: { value: {x: windData.uMax, y: windData.vMax} },
        },
        vertexShader: drawVert,
        fragmentShader: drawFrag,
        depthTest: false,
        transparent: true,
    } );
    const pointsWind = new THREE.Points( pointsWindGeo, materialDraw );
    // scene.add( pointsWind );
    sceneBack.add(pointsWind);
    planeMaterial.map = materialScreen.uniforms.u_screen.value;

    function findRenderTargetByTexture(texture) {
        const targets = [backgroundTarget, screenTarget]
        return targets.find(target => target.texture === texture);
    }
    function findScreenRenderTargetNot(texture) {
        const targets = [backgroundTarget, screenTarget]
        return targets.find(target => target.texture !== texture);
    }

    function animate() {
        // materialScreen.blending = THREE.CustomBlending
        const windTarget = findRenderTargetByTexture(materialScreen.uniforms.u_screen.value);
        renderer.setRenderTarget( windTarget );
        renderer.render( sceneBack, _camera );
        renderer.setRenderTarget( null )
        // renderer.clear();
        renderer.render( sceneFront, _camera );
        materialBg.uniforms.u_screen.value = findScreenRenderTargetNot(materialBg.uniforms.u_screen.value).texture
        materialScreen.uniforms.u_screen.value = findScreenRenderTargetNot(materialScreen.uniforms.u_screen.value).texture
        // materialScreen.blending = THREE.NoBlending
        drawUpdateParticles()

        renderer.setRenderTarget( null )
        renderer.clear()
        renderer.render( scene, camera )
        requestAnimationFrame(animate)
    }
    animate()
</script>
</body>
</html>
